/* Copyright (c) 2011-2013 ISP RAS, Moscow. For more information please write to

      mermakov@ispras.ru
      svartanov@ispras.ru

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package ru.ispras.coffeemachine.instrument;

import java.util.ArrayList;
import java.util.List;

import org.apache.bcel.Constants;
import org.apache.bcel.generic.ACONST_NULL;
import org.apache.bcel.generic.ALOAD;
import org.apache.bcel.generic.BasicType;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.D2L;
import org.apache.bcel.generic.DUP;
import org.apache.bcel.generic.DUP2;
import org.apache.bcel.generic.DUP2_X1;
import org.apache.bcel.generic.DUP_X2;
import org.apache.bcel.generic.F2L;
import org.apache.bcel.generic.GETSTATIC;
import org.apache.bcel.generic.IASTORE;
import org.apache.bcel.generic.ICONST;
import org.apache.bcel.generic.INVOKESTATIC;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.NEWARRAY;
import org.apache.bcel.generic.POP;
import org.apache.bcel.generic.PUSH;

import ru.ispras.coffeemachine.instrument.util.Utility;
import ru.ispras.coffeemachine.target.Bytecode;
import ru.ispras.coffeemachine.target.Type;
import ru.ispras.coffeemachine.util.FrameworkUtil;

public class DaemonMilk extends Milk
{
	protected boolean isStarter = false;
	protected boolean isFinisher = false;

	protected int invokeIn;
	protected Type invokeOut;

	protected int triggeredNew = 0;

	List<String> listenerClasses;

	public DaemonMilk(ClassGen targetClass, ConstantPoolGen cpg)
	{
		super(targetClass, cpg);
		className = "Daemon";

		listenerClasses = new ArrayList<String>();

		for (String interfaceName : targetClass.getInterfaceNames()) {
			if (FrameworkUtil.isListenerClass(interfaceName)) {
				listenerClasses.add(interfaceName);
			}
		}
	}

	@Override
	protected InstructionList createPreList()
	{
		InstructionList result = new InstructionList();
		if (activeInvoke != null) {
			result.append(new PUSH(cpg, invokeIn));
			if (invokeOut == null) {
				result.append(new ACONST_NULL());
			} else {
				result.append(new GETSTATIC(cpg.addFieldref("ru/ispras/coffeemachine/target/Type", invokeOut.toString(), "Lru/ispras/coffeemachine/target/Type;")));
			}
			result.append(new PUSH(cpg, activeInvoke));
			appendMethod("simulateWrap", "(ILru/ispras/coffeemachine/target/Type;I)V", result);
			activeInvoke = null;
		}

		// FIXME: for debug
		// createMethod("printState", "()V", result);

		if (result.size() == 0)
		{
			return null;
		}
		return result;
	}

	@Override
	protected InstructionList createExceptionHandlerList() {
		InstructionList result = new InstructionList();
		appendMethod("catchThrow", "()V", result);

		return result;
	}

	@Override
	protected InstructionList createFirstList()
	{
		InstructionList result = new InstructionList();
		if (currentMethod.isStatic() || currentMethod.isConstructor()) {
			appendMethod("init", "()V", result);
		}
		result.append(new PUSH(cpg, currentMethod.getMaxLocals()));
		appendMethod("pushLocal", "(I)V", result);
		result.append(factory.pushIntArray(getSizeSignature(currentMethod.getSignature(),
		                                                   currentMethod.isStatic())));
		result.append(new PUSH(cpg, getSignatureHash()));
		appendMethod("pushParams", "([II)V", result);
		if (!currentMethod.isStatic() && !currentMethod.getName().contains("<clinit>")) {
			if (!currentMethod.getName().contains("<init>")) {
				result.append(new ALOAD(0));
				appendMethod("updateThis", "(Ljava/lang/Object;)V", result);
			}
		}

		if (isStarter)
		{
			appendMethod("eventstart", "()V", result);
			isStarter = false;
		}
		return result;
	}

	/*
	 * Instructions
	 */

	@Override
	protected void pushnull()
	{
		appendMethod("pushnull", "()V");
	}

	@Override
	protected void pushconst(Type t) {

		appendMethod("ldc" + t.toString(), "()V");
	}

	@Override
	protected void xload(int index, Type type) {

		instructionList.append(new PUSH(cpg, index));
		appendMethod("xload", "(I)V");
	}

	@Override
	protected void xaload(Type type)
	{
		instructionList.append(new DUP());
		instructionList.append(new GETSTATIC(cpg.addFieldref("ru/ispras/coffeemachine/target/Type",
		                                                     type.toString(),
		                                                     "Lru/ispras/coffeemachine/target/Type;")));
		appendMethod("xaload", "(ILru/ispras/coffeemachine/target/Type;)V");
	}

	@Override
	protected void xstore(int index, Type t) {

		instructionList.append(new PUSH(cpg, index));
		instructionList.append(new PUSH(cpg, t.size()));
		appendMethod("xstore", "(II)V");
	}

	@Override
	protected void xastore(Type type)
	{
		String storeType;
		String shortType;
		if (type.isScalar()) {
			storeType = type.toString();
			shortType = storeType;
		} else {
			storeType = "Ljava/lang/Object;";
			shortType = "R";
		}
		if (type.equals(Type.B) || type.equals(Type.Z)) {
			instructionList.append(new DUP_X2());
		}
		appendMethod("xastoreHelper", "(" + storeType + ")V");
		if (type.equals(Type.B) || type.equals(Type.Z)) {
			instructionList.append(new DUP2_X1());
			storeType = "V";
		} else {
			instructionList.append(new DUP2());
		}
		updateCodePosition();
		appendMethod("xastore" + shortType, "(Ljava/lang/Object;II)" + storeType);
	}

	@Override
	protected void pop()
	{
		appendMethod("pop", "()V");
	}

	@Override
	protected void pop2()
	{
		appendMethod("pop2", "()V");
	}

	@Override
	protected void dup(int num, int before) {

		String funcName = "dup";
		if (num > 1) {
			funcName += num;
		}
		if (before > 0) {
			funcName += "_x" + before;
		}
		appendMethod(funcName, "()V");
	}

	@Override
	protected void swap()
	{
		appendMethod("swap", "()V");
	}

	/**
	 * Arithmetic operation for parameters of types integer and long
	 */
	@Override
	protected void arithmetic(Bytecode kind, Type... types) {

		int[] indexes = new int[types.length];
		InstructionList[] extra = new InstructionList[types.length];
		for (int i = 0; i < types.length; i ++) {
			indexes[i] = i;
			extra[i] = null;
		}
		instructionList.append(factory.getElements(types, indexes, extra, currentMethod.getMaxLocals()));
		if (kind == Bytecode.UMINUS) {
			instructionList.append(factory.pushConstant(types[0], 0));
		}
		instructionList.append(new GETSTATIC(cpg.addFieldref(targetPackage + "Bytecode",
		                                                     kind.toString(), bytecodeClass)));
		if (kind == Bytecode.UMINUS) {
			appendMethod("binary", "(" + types[0].toString() + types[0].toString() + bytecodeClass + ")V");
		} else {
			appendMethod("binary", "(" + types[0].toString() + types[1].toString() + bytecodeClass + ")V");
		}
	}

	@Override
	protected void iinc(int index, int constant)
	{
		instructionList.append(new PUSH(cpg, index));
		instructionList.append(new PUSH(cpg, constant));
		appendMethod("iinc", "(II)V");
	}

	@Override
	protected void convert(Type from, Type to)
	{
		instructionList.append(new GETSTATIC(cpg.addFieldref(targetPackage + "Type",
		                                                     from.toString(),
		                                                     typeClass)));
		instructionList.append(new GETSTATIC(cpg.addFieldref(targetPackage + "Type",
		                                                     to.toString(),
		                                                     typeClass)));
		appendMethod("convert", "(" + typeClass + typeClass + ")V");
	}

	@Override
	protected void cmp(Type type) {

		Type []types = {type, type};
		int []indexes = {0, 1};
		InstructionList []before = new InstructionList[2];
		InstructionList []after = new InstructionList[2];
		for (int i = 0; i < 2; i ++) {
			if (type == Type.F) {
				after[i] = new InstructionList(new F2L());
			} else if (type == Type.D) {
				after[i] = new InstructionList(new D2L());
			} else if (type == Type.J) {
				after[i] = new InstructionList();
			}
		}
		instructionList.append(factory.getElements(types, indexes,
		                                           after, before, currentMethod.getMaxLocals()));
		appendMethod("cmp", "(JJ)V");
	}

	@Override
	protected void zeroCompare(Bytecode type)
	{
		instructionList.append(new DUP());
		instructionList.append(new ICONST(0));
		instructionList.append(new GETSTATIC(cpg.addFieldref("ru/ispras/coffeemachine/target/Bytecode",
		                                                     type.toString(),
		                                                     "Lru/ispras/coffeemachine/target/Bytecode;")));
		instructionList.append(new PUSH(cpg, true));
		appendMethod("compare", "(IILru/ispras/coffeemachine/target/Bytecode;Z)V");
	}

	@Override
	protected void compare(Bytecode type) {

		instructionList.append(new DUP2());
		instructionList.append(new GETSTATIC(cpg.addFieldref("ru/ispras/coffeemachine/target/Bytecode", type.toString(), "Lru/ispras/coffeemachine/target/Bytecode;")));
		instructionList.append(new PUSH(cpg, false));
		appendMethod("compare", "(IILru/ispras/coffeemachine/target/Bytecode;Z)V");
	}

	@Override
	protected void compareRefs()
	{
		instructionList.append(new DUP2());
		appendMethod("acompare", "(Ljava/lang/Object;Ljava/lang/Object;)V");
	}

	@Override
	protected void xreturn(boolean popStack)
	{
		if (isInit()) {
			instructionList.append(new ALOAD(0));
			appendMethod("updateObject", "(Ljava/lang/Object;)V");
		}
		if (isFinisher)
		{
			appendMethod("eventend", "()V");
		}
		instructionList.append(new PUSH(cpg, popStack));
		appendMethod("xreturn", "(Z)V");
	}

	@Override
	protected void getstatic(Object[] arguments)
	{
		instructionList.append(new PUSH(cpg, (String) arguments[0]));
		appendMethod("getstatic", "(Ljava/lang/String;)V");
	}

	@Override
	protected void putstatic(Object[] arguments) {

		Type type = Type.fromString((String)arguments[1]);
		if (type != null) {
			if (type.getItemSize() == 1) {
				instructionList.append(new DUP());
			} else {
				instructionList.append(new DUP2());
			}

			putField(type, (String)arguments[0], false, true);
		} else {
			// FIXME
		}
	}

	@Override
	protected void getfield(Object[] arguments)
	{
		instructionList.append(new DUP());
		instructionList.append(new PUSH(cpg, (String)arguments[0]));
		appendMethod("getfield", "(Ljava/lang/Object;Ljava/lang/String;)V");
	}

	@Override
	protected void putfield(Object[] arguments) {

		Type type = Type.fromString((String)arguments[1]);
		boolean isDelayed = false;
		if (type != null) {
			Type[] types = { Type.R, type };
			int[] indexes = { 0, 1 };
			InstructionList getHash = new InstructionList();
            if (isInit() && ((String)arguments[0]).contains(currentMethod.getClassName().replace("/", "."))) {
                    getHash.append(new POP());
                    getHash.append(new ICONST(0));
                    isDelayed = true;
            } else {
                    getHash.append(new INVOKESTATIC(cpg.addMethodref("java.lang.System", "identityHashCode", "(Ljava/lang/Object;)I")));
            }
            InstructionList[] extra = { getHash, null };
            instructionList.append(factory.getElements(types, indexes, extra, currentMethod.getMaxLocals()));
			putField(type, (String)arguments[0], isDelayed, false);
		} else {
			// FIXME
		}
	}

	protected void putField(Type type, String name, boolean isDelayed, boolean isStatic) {

		instructionList.append(factory.expandPrimitive(type));
		instructionList.append(new PUSH(cpg, name));
		String firstParam;
		if (type.isFloat()) {
			firstParam = Type.D.toString();
		} else if (type.isInteger()) {
			firstParam = Type.J.toString();
		} else {
			firstParam = "Ljava/lang/Object;";
		}
		instructionList.append(new GETSTATIC(cpg.addFieldref("ru/ispras/coffeemachine/target/Type",
		                                                     type.toString(), typeClass)));
		if (!isStatic) {
			instructionList.append(new PUSH(cpg, isDelayed));
		}
		instructionList.append(new PUSH(cpg, Utility.isFinalVariable(targetClass.getJavaClass(), name)));
		if (!isStatic) {
			appendMethod("putfield",
			             "(I" + firstParam + "Ljava/lang/String;" + typeClass + "ZZ)V");
		} else {
			appendMethod("putstatic", "(" + firstParam + "Ljava/lang/String;" + typeClass + "Z)V");
		}
	}

	/**
	 * Method invocation. There are 3 classes of methods: open methods (bytecode
	 * occurs in trace), close methods without taint results (simulate), close
	 * methods with taint result (interpret).
	 * <p>
	 * Thread identifier detection for <tt>run()</tt> or <tt>start()</tt>
	 * methods.
	 *
	 * @param type method type (0 for virtual, 1 for special, 2 for static and 3
	 *        for interface)
	 */
	@Override
	protected void invoke(Object []parameters, short type)
	{
		String className = (String)parameters[0];
		String method = (String)parameters[1];
		String signature = (String)parameters[2];

		int start = signature.indexOf('(');
		int end = signature.indexOf(')');

		invokeIn = Type.parseSignature(signature.substring(start + 1, end)).size() +
										((type == Constants.INVOKESTATIC) ? 0 : 1);
		invokeOut = Type.fromString(signature.substring(end + 1));

		activeInvoke = (method + signature).hashCode();
		instructionList.append(new PUSH(cpg, activeInvoke));

		appendMethod("addInvoke", "(I)V");

		className = getRealClassName(className, method, signature);

		InstructionList inv = Invoker.createBeforeList(className, method, signature);

		instructionList.append(inv);
	}

	@Override
	protected void xnew(String className) {

		instructionList.append(new PUSH(cpg, className));
		appendMethod("xnew", "(Ljava/lang/String;)V");
		triggeredNew ++;
	}

	@Override
	protected void newarray(String type)
	{
		instructionList.append(new DUP());
		appendMethod("newarray", "(I)V");
	}

	@Override
	protected void xinstanceof(String className)
	{
		instructionList.append(new DUP());
		instructionList.append(new PUSH(cpg, className));

		appendMethod("xinstanceof", "(Ljava/lang/Object;Ljava/lang/String;)V");
	}

	@Override
	protected void arraylength()
	{
		appendMethod("arraylength", "()V");
	}

	@Override
	protected void monitor(boolean isEnter)
	{
		appendMethod("pop", "()V");
	}

	@Override
	protected void multianewarray(int dimensions)
	{
		InstructionList []before = new InstructionList[dimensions];
		InstructionList []after = new InstructionList[dimensions];
		int []indexes = new int[dimensions];
		Type []types = new Type[dimensions];
		for (int i = 0; i < dimensions; i ++) {
			types[i] = Type.I;
			indexes[i] = i;
			if (i == 0) {
				before[i] = new InstructionList(new PUSH(cpg, dimensions));
				before[i].append(new NEWARRAY(BasicType.INT));
			} else {
				before[i] = new InstructionList();
			}
			before[i].append(new DUP());
			before[i].append(new ICONST(i));
			after[i] = new InstructionList(new IASTORE());
		}
		instructionList.append(factory.getElements(types, indexes, after, before, currentMethod.getMaxLocals()));

		appendMethod("multianewarray", "([I)V");
	}

	@Override
	protected void nullCompare(boolean isInverted) {

		instructionList.append(new DUP());
		instructionList.append(new PUSH(cpg, isInverted));
		appendMethod("nullcompare", "(Ljava/lang/Object;Z)V");
	}

	/*
	 * After instructions
	 */

	@Override
	protected void invokeAfter(Object []parameters, short type)
	{
		String className = (String)parameters[0];
		String method = (String)parameters[1];
		String signature = (String)parameters[2];

		if ((type == Constants.INVOKESPECIAL) && (triggeredNew > 0) && (method.equals("<init>")))
		{
			triggeredNew--;
			appendMethod("setObjectFields", "(Ljava/lang/Object;)V");
		}
		className = getRealClassName(className, method, signature);

		InstructionList inv = Invoker.createAfterList(className, method, signature);

		instructionList.append(inv);
	}

	@Override
	protected void xnewAfter()
	{
		instructionList.append(new DUP());
	}

	@Override
	protected void setCurrentMethod(CoffeeMethodGen currentMethod) {
		super.setCurrentMethod(currentMethod);
		for (String listenerClass : listenerClasses) {
			FrameworkUtil.ListenerType type = FrameworkUtil.getListenerType(listenerClass, currentMethod.getName());
			if (type != null) {
				isFinisher = type.isFinisher();
				isStarter = type.isStarter();
			}
		}
	}
}
